Skip to content

Add command result support for resource commands#15622

Open
davidfowl wants to merge 1 commit intomainfrom
davidfowl/command-results
Open

Add command result support for resource commands#15622
davidfowl wants to merge 1 commit intomainfrom
davidfowl/command-results

Conversation

@davidfowl
Copy link
Copy Markdown
Contributor

@davidfowl davidfowl commented Mar 26, 2026

Description

Adds Result and ResultFormat properties to ExecuteCommandResult, allowing resource commands to return structured output data (text or JSON) back to the caller. The result flows through the entire pipeline: model → proto/gRPC → backchannel → Dashboard UI → CLI → MCP tools.

Model: New CommandResultFormat enum (None, Text, Json), Result/ResultFormat on ExecuteCommandResult, and CommandResults.Success(result, format) overload.

Proto/gRPC: ResourceCommandResponse gets optional string result and CommandResultFormat result_format fields.

Backchannel: ExecuteResourceCommandResponse gains Result and ResultFormat (string) properties.

Dashboard: On command success with result data, opens TextVisualizerDialog with syntax highlighting. JSON results lock the viewer to JSON mode.

CLI: Result written to stdout via DisplayRawText(result, ConsoleOutput.Standard). Status messages routed to stderr, enabling clean piping (e.g., aspire resource myResource generate-token | jq .token). MCP tool returns result as additional TextContentBlock.

Replica aggregation: Takes the first successful replica's result when multiple replicas return data.

Playground: Added "Generate Token" (JSON) and "Get Connection String" (text) demo commands on stress-telemetryservice.

Fixes #15610

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15622

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15622"

@afscrome
Copy link
Copy Markdown
Contributor

Something I've been playing around with in my JWT scenario is writing the output directly to the user's clipboard - something worth thinking about. Works reasonably well for secrets

@davidfowl davidfowl force-pushed the davidfowl/command-results branch 2 times, most recently from 2f9c523 to fc149ed Compare March 26, 2026 23:08
@davidfowl davidfowl marked this pull request as ready for review March 27, 2026 01:19
Copilot AI review requested due to automatic review settings March 27, 2026 01:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end support for resource commands to return structured result data (text/JSON) back to callers (Dashboard, CLI, MCP), flowing from hosting model → backchannel/gRPC → UI/CLI.

Changes:

  • Introduces CommandResultFormat and extends ExecuteCommandResult/CommandResults.Success(...) to carry result payload + format.
  • Extends dashboard gRPC/backchannel contracts and mapping to transport result/result_format.
  • Updates Dashboard/CLI/MCP to surface command results, plus adds/updates tests and regenerated polyglot codegen snapshots.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Aspire.Hosting.Tests/ResourceCommandServiceTests.cs Adds hosting tests for result propagation and replica aggregation behavior.
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts Updates TS generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs Updates Rust generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py Updates Python generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java Updates Java generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go Updates Go generated API snapshots for new fields/enum.
tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs Adds a hook to capture DisplayMessage calls in CLI tests.
tests/Aspire.Cli.Tests/Mcp/ExecuteResourceCommandToolTests.cs Verifies MCP tool returns additional content block when a command returns result data.
tests/Aspire.Cli.Tests/Commands/ResourceCommandHelperTests.cs Adds CLI tests around displaying command results.
src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto Adds result and result_format fields + CommandResultFormat enum to the dashboard proto.
src/Aspire.Hosting/Dashboard/DashboardServiceData.cs Extends command execution tuple to include result payload + format.
src/Aspire.Hosting/Dashboard/DashboardService.cs Maps hosting command results into gRPC response fields.
src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs Adds backchannel DTO properties for result payload + format.
src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs Maps hosting ExecuteCommandResult to backchannel result fields.
src/Aspire.Hosting/ApplicationModel/ResourceCommandService.cs Aggregates replica results and returns the first successful result payload/format.
src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs Adds CommandResultFormat, ExecuteCommandResult.Result/ResultFormat, and CommandResults.Success(...) overload.
src/Aspire.Dashboard/ServiceClient/Partials.cs Maps gRPC result fields into dashboard view model.
src/Aspire.Dashboard/Model/ResourceCommandResponseViewModel.cs Adds result payload + format to the dashboard model and defines CommandResultFormat.
src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs Opens a text visualizer dialog on successful command results (locks to JSON when applicable).
src/Aspire.Cli/Mcp/Tools/ExecuteResourceCommandTool.cs Returns command result as an additional MCP TextContentBlock on success.
src/Aspire.Cli/Commands/ResourceCommandHelper.cs Displays command result in CLI after successful execution.
playground/Stress/Stress.AppHost/Program.cs Adds sample commands returning JSON/text results in the stress playground.

Copy link
Copy Markdown
Member

@JamesNK JamesNK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review

@davidfowl davidfowl force-pushed the davidfowl/command-results branch from 6c96bb0 to 4619b34 Compare March 27, 2026 04:57
@davidfowl davidfowl force-pushed the davidfowl/command-results branch from 4619b34 to 0898d6d Compare March 27, 2026 05:04
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl force-pushed the davidfowl/command-results branch from 968ae8f to 5f7901c Compare March 27, 2026 18:02
@github-actions
Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

{
return new ExecuteCommandResult { Success = true };
var successWithResult = results.FirstOrDefault(r => r.Result is not null);
return new ExecuteCommandResult
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if there are different replicas of a resource? Should the result aggregate all results and send those back in an array of results or something like that?

Copy link
Copy Markdown
Member

@adamint adamint Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re @joperezr's replicas comment, yes, it should - maybe also have the ability to run a command on only one instance?

@github-actions
Copy link
Copy Markdown
Contributor

🎬 CLI E2E Test Recordings — 52 recordings uploaded (commit 5f7901c)

View recordings
Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJavaEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateJavaAppHostWithViteApp ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RunWithMissingAwaitShowsHelpfulError ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #23660492243

message ResourceCommandResponse {
ResourceCommandResponseKind kind = 1;
optional string error_message = 2;
optional string result = 3;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when apphost and dashboard are different versions? Like for instance, with ACA or standalone dashboard? Should we be revving the api version?

logger.LogDebug("Executing command '{CommandName}' on resource '{ResourceName}'", commandName, resourceName);

// Route status messages to stderr so command results can be piped (e.g., | jq)
interactionService.Console = ConsoleOutput.Error;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem right, shouldn't we be adding a parameter to ShowStatusAsync that accepts ConsoleOutput, like DisplayRawText does?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we want all messages to go to stderr and output to be stdout. This is similar to what we do when —format json is specified

/// <summary>
/// No result data.
/// </summary>
None = 0,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this necessary if result is nulalble? resultformat could be nullable as well? or we could encapsulate result and resultformat in a wrapper type, like CommandResult

/// <summary>
/// No result data.
/// </summary>
None = 0,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment

{
return new ExecuteCommandResult { Success = true };
var successWithResult = results.FirstOrDefault(r => r.Result is not null);
return new ExecuteCommandResult
Copy link
Copy Markdown
Member

@adamint adamint Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re @joperezr's replicas comment, yes, it should - maybe also have the ability to run a command on only one instance?

/// <summary>
/// Gets the result data produced by the command.
/// </summary>
public string? Result { get; init; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enapsulate w/format?

ResourceCommandResponseKind kind = 1;
optional string error_message = 2;
optional string result = 3;
CommandResultFormat result_format = 4;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be optional too? it's elsewhere nullable

}

enum CommandResultFormat {
COMMAND_RESULT_FORMAT_NONE = 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mentioned above, having the none type feels strange

/// </summary>
/// <param name="result">The result data.</param>
/// <param name="resultFormat">The format of the result data. Defaults to <see cref="CommandResultFormat.Text"/>.</param>
public static ExecuteCommandResult Success(string result, CommandResultFormat resultFormat = CommandResultFormat.Text) => new() { Success = true, Result = result, ResultFormat = resultFormat };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should result be nullable here, to match that we allow null results in the model?

/// <param name="errorMessage">The error message.</param>
/// <param name="result">The result data.</param>
/// <param name="resultFormat">The format of the result data. Defaults to <see cref="CommandResultFormat.Text"/>.</param>
public static ExecuteCommandResult Failure(string errorMessage, string result, CommandResultFormat resultFormat = CommandResultFormat.Text) => new() { Success = false, ErrorMessage = errorMessage, Result = result, ResultFormat = resultFormat };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, should errorMessage and result be nullable?

{
Kind = responseKind,
ErrorMessage = errorMessage ?? string.Empty
ErrorMessage = errorMessage ?? string.Empty,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also use null to represent "no error message"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a grpc quirk

Kind = responseKind,
ErrorMessage = errorMessage ?? string.Empty
ErrorMessage = errorMessage ?? string.Empty,
ResultFormat = (Aspire.DashboardService.Proto.V1.CommandResultFormat)resultFormat
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the various types for CommandResultFormat always change in sync? Using an explicit switch to map the values would help catching if we miss doing that. Here and the other places where we cast for similar types.

{
logger.LogDebug("Executing command '{CommandName}' on resource '{ResourceName}'", commandName, resourceName);

// Route status messages to stderr so command results can be piped (e.g., | jq)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Route status messages to stderr so command results can be piped (e.g., | jq)
// Route status messages to stderr so command results in stdout remain pipeable (e.g., | jq)

Comment on lines +62 to +63
// Route status messages to stderr so command results can be piped (e.g., | jq)
interactionService.Console = ConsoleOutput.Error;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a focused test for the new stream-routing behavior here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve using commands from the CLI or MCP

7 participants